// Copyright 2000-2018 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
package com.neuronbit.businessflow.intellij.editor.swing.editor;

import com.intellij.openapi.application.ReadAction;
import com.intellij.openapi.project.Project;
import com.intellij.psi.PsiClass;
import com.intellij.psi.PsiType;
import com.intellij.psi.impl.java.stubs.index.JavaFullClassNameIndex;
import com.intellij.psi.search.GlobalSearchScope;
import com.intellij.util.concurrency.AppExecutorUtil;
import com.mxgraph.model.mxCell;
import com.mxgraph.model.mxICell;
import com.neuronbit.businessflow.intellij.model.*;
import com.neuronbit.businessflow.intellij.util.PsiUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.concurrency.CancellablePromise;

import java.util.*;
import java.util.concurrent.ExecutionException;

/**
 * The type Rad java method component.
 */
public abstract class RadJavaMethodComponent extends RadComponent {

    /**
     * Instantiates a new Rad java method component.
     *
     * @param module the module
     * @param cell   the cell
     * @param id     the id
     */
    public RadJavaMethodComponent(ModuleProvider module, @NotNull mxCell cell, @NotNull String id) {
        super(module, cell, id);
    }

    /**
     * Instantiates a new Rad java method component.
     *
     * @param module  the module
     * @param cell    the cell
     * @param id      the id
     * @param palette the palette
     */
    public RadJavaMethodComponent(ModuleProvider module, @NotNull mxCell cell, @NotNull String id, Palette palette) {
        super(module, cell, id, palette);
    }

    /**
     * Gets name.
     *
     * @return the name
     */
    public String getName() {
        if (getDelegee().getValue() == null) {
            return null;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        return task.getName();
    }

    /**
     * Gets class to bind.
     *
     * @return the class to bind
     */
    public String getClassToBind() {
        if (getDelegee().getValue() == null) {
            return null;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        Type type = task.getCallable().getType();
        if (null == type) {
            return "";
        }
        return type.getQualifiedName();
    }

    /**
     * Sets class to bind.
     *
     * @param classToBind the class to bind
     */
    public void setClassToBind(String classToBind) {
        if (getDelegee().getValue() == null) {
            return;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        task.getCallable().setType(SimpleType.of(classToBind));
    }

    /**
     * Is flow boolean.
     *
     * @return the boolean
     */
    public boolean isFlow() {
        if (getDelegee().getValue() == null) {
            return false;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        return task.getNodeType() == NodeType.FLOW;
    }

    /**
     * Gets method to bind.
     *
     * @return the method to bind
     */
    public FormattedMethodString getMethodToBind() {
        if (getDelegee().getValue() == null) {
            return null;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        FormattedMethodString method = task.getCallable().getMethod();
        if (null != method) {
            return method;
        } else {
            return null;
        }
    }

    /**
     * Gets flow returns.
     *
     * @return the flow returns
     */
    public @Nullable Set<Return> getFlowReturns() {
        Call call = getFlowCallable();
        if (null != call) {
            return call.getReturns();
        } else {
            return null;
        }
    }

    /**
     * Gets flow callable.
     *
     * @return the flow callable
     */
    public @Nullable Call getFlowCallable() {
        mxICell cell = this.getDelegee();
        mxICell root = null;
        while (true) {
            mxICell parent = cell.getParent();
            if (parent == null) {
                break;
            }
            if (parent.getId().equals("0")) {
                root = parent;
                break;
            }
            cell = parent;
        }
        if (root == null) {
            return null;
        }

        Object value = root.getValue();
        if (value instanceof Flow) {
            Flow flow = (Flow) value;
            if (flow.getCallable() == null) {
                return null;
            }

            return flow.getCallable();
        } else {
            return null;
        }
    }

    /**
     * Gets flow return type.
     *
     * @return the flow return type
     */
    public @Nullable PsiType getFlowReturnType() {
        Call call = getFlowCallable();
        if (null == call) {
            return null;
        }

        if (null == call.getType() || call.getMethod() == null || call.getMethod().isBlank()) {
            return null;
        }
        Project project = getProject();
        CancellablePromise<PsiType> submitted = ReadAction.nonBlocking(() -> {
            Collection<PsiClass> classes = JavaFullClassNameIndex.getInstance()
                    .getClasses(call.getMethod().getReturnType(), project, GlobalSearchScope.allScope(project));
            if (classes.isEmpty()) {
                return null;
            }

            for (PsiClass aClass : classes) {
                return PsiUtils.getPsiTypeFromPsiClass(aClass);
            }
            return null;
        }).inSmartMode(project).submit(AppExecutorUtil.getAppExecutorService());

        try {
            return submitted.get();
        } catch (InterruptedException | ExecutionException e) {
            return null;
        }
    }

    /**
     * Sets method to bind.
     *
     * @param methodToBind the method to bind
     */
    public void setMethodToBind(String methodToBind) {
        if (getDelegee().getValue() == null) {
            return;
        }

        FormattedMethodString method = new FormattedMethodString(methodToBind);

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();

        Call callable = task.getCallable();
        callable.setMethod(method);

        Map<String, Param> params = callable.getParams();
        Map<String, Param> oldParams = new TreeMap<>(params);
        params.clear();

        String[] parameterTypes = method.getParameterTypes();
        for (int i = 0; i < parameterTypes.length; i++) {
            Param param = oldParams.get(String.valueOf(i));
            if (param != null) {
                params.put(String.valueOf(i), param);
            } else {
                params.put(String.valueOf(i), new Param(String.valueOf(i), ""));
            }
        }
    }

    /**
     * Gets param value.
     *
     * @param index the index
     * @return the param value
     */
    public String getParamValue(String index) {
        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        return task.getCallable().getParamValue(index);
    }

    /**
     * Sets param value.
     *
     * @param index the index
     * @param value the value
     */
    public void setParamValue(String index, String value) {
        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        task.getCallable().setParam(index, value);
    }

    /**
     * Gets return value.
     *
     * @param name the name
     * @return the return value
     */
    public String getReturnValue(String name) {
        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        return task.getCallable().getReturnValue(name);
    }

    /**
     * Sets return value.
     *
     * @param name  the name
     * @param value the value
     */
    public void setReturnValue(String name, String value) {
        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        task.getCallable().setReturn(name, value);
    }

    /**
     * Sets flow return value.
     *
     * @param componentName       the component name
     * @param fieldName           the field name
     * @param flowReturnFieldName the flow return field name
     */
    public void setFlowReturnValue(String componentName, String fieldName, String flowReturnFieldName) {
        Call call = getFlowCallable();
        Set<Return> returns = Objects.requireNonNull(call).getReturns();

        Return r = new Return(flowReturnFieldName, componentName + ("output".equals(fieldName) ? "." : ".output.") + fieldName);
        returns.remove(r);
        returns.add(r);
    }

    /**
     * Gets exception handler class to bind.
     *
     * @return the exception handler class to bind
     */
    public String getExceptionHandlerClassToBind() {
        if (getDelegee().getValue() == null) {
            return null;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        Type type = task.getExceptionHandler().getType();
        if (null == type) {
            return "";
        }
        return type.getQualifiedName();
    }

    /**
     * Sets exception handler class to bind.
     *
     * @param classToBind the class to bind
     */
    public void setExceptionHandlerClassToBind(String classToBind) {
        if (getDelegee().getValue() == null) {
            return;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        task.getExceptionHandler().setType(SimpleType.of(classToBind));
    }

    /**
     * Gets exception handler method to bind.
     *
     * @return the exception handler method to bind
     */
    public String getExceptionHandlerMethodToBind() {
        if (getDelegee().getValue() == null) {
            return null;
        }

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();
        FormattedMethodString method = task.getExceptionHandler().getMethod();
        if (null != method) {
            return method.getValue();
        } else {
            return null;
        }
    }

    /**
     * Sets exception handler method to bind.
     *
     * @param methodToBind the method to bind
     */
    public void setExceptionHandlerMethodToBind(String methodToBind) {
        if (getDelegee().getValue() == null) {
            return;
        }

        FormattedMethodString method = new FormattedMethodString(methodToBind);

        JavaMethodTask task = (JavaMethodTask) getDelegee().getValue();

        ExceptionHandler callable = task.getExceptionHandler();
        callable.setMethod(method);
    }
}
